package uw10.app.net;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import android.util.Log;

import uw10.app.data.Event;
import uw10.app.data.GraphBounds;
import uw10.app.data.GraphData;
import uw10.app.data.GraphIndicator;
import uw10.app.data.sources.EventGraphDataSource;
import uw10.app.data.sources.EventRatingFeedbackHandler;
import uw10.app.data.sources.EventSource;
import uw10.app.data.sources.RecentPrioritySource;

public class ClientServerManager implements EventRatingFeedbackHandler, RecentPrioritySource, EventSource, EventGraphDataSource
{
	private volatile long lastUpdateTime;
	
	private volatile String pingServer;
	private volatile int pingPort;
	
	private volatile String httpServer;
	
	private Event lastEvent;
	private EventQueryResults lastEventResults;
	private Map<Event, EventQueryResults> eventResultCache = new HashMap<Event, EventQueryResults>();
	
	public long getLastUpdateTime()
	{
		return lastUpdateTime;
	}
	
	public void setLastUpdateTime(long lastUpdateTime)
	{
		this.lastUpdateTime = lastUpdateTime;
	}
	
	public void setupPing(String server, int port)
	{
		pingServer = server.split("/")[0];
		pingPort = port;
	}
	
	public void setupHttp(String server, int port)
	{
		if (server.startsWith("http://"))
		{
			server = server.replaceFirst("http://","");
		}
		
		if (server.contains("/"))
			server = server.replaceFirst("/", ":" + port + "/");
		else
			server += ":" + port;
		
		httpServer = "http://" + server;
	}
	
	public int getRecentPriority()
	{
		int priority = -1;

		Socket sock = null;
		try
		{

			sock = new Socket();
			sock.setSoTimeout(10000);
			sock.connect(new InetSocketAddress(pingServer.split("/", 1)[0], pingPort), 10000);

			OutputStream out = sock.getOutputStream();
			
			long t = lastUpdateTime;
			byte[] message = new byte[4];
			for (int i = 0; i < 4; i++)
			{
				message[i] = (byte)(t & 255);
				t = t >> 8;
			}
			out.write(message);
			
			InputStream in = sock.getInputStream();
			
			priority = in.read();
		}
		catch (Exception e)
		{
			Log.w("Recent Priority Exception", e.toString());
		}
		
		try
		{
			if (sock != null) sock.close();
		} catch (Exception e) { }
		
		return priority;
	}
	
	private void updateEventQueryResults(Event e)
	{
		if (e != lastEvent)
		{
			if (eventResultCache.containsKey(e))
			{
				lastEvent = e;
				lastEventResults = eventResultCache.get(e);
			}
			else
			{
				lastEvent = e;
				lastEventResults = new EventQueryResults();
				
				lastEventResults.title = e.company + " at " + e.getNiceTime();
				
				HttpParams params = new BasicHttpParams();
				HttpConnectionParams.setConnectionTimeout(params, 5000);
				HttpConnectionParams.setSoTimeout(params, 5000);
				HttpClient client = new DefaultHttpClient(params);
				try
				{
			        HttpGet request = new HttpGet();
			        request.setURI(new URI(httpServer + "/eventgraph.php?id=" + e.id));
			        HttpResponse response = client.execute(request);
			        
			        ArrayList<Long>bid_time = new ArrayList<Long>();
			        ArrayList<Double>bid_value = new ArrayList<Double>();
			        ArrayList<Long>ask_time = new ArrayList<Long>();
			        ArrayList<Double>ask_value = new ArrayList<Double>();
			        ArrayList<Long>news_time = new ArrayList<Long>();
			        ArrayList<Double>news_value = new ArrayList<Double>();
			        
			        BufferedReader in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
			        String line;
			        while ((line = in.readLine()) != null)
			        {
			        	// type,time,value
			        	String[] args = line.split(",");
			        	if (args.length == 3)
			        	{
			        		try
			        		{
			        			Long timestamp = Long.parseLong(args[1]);
			        			Double value = Double.parseDouble(args[2]);
			        			
			        			if (args[0].equals("b"))
			        			{
			        				bid_time.add(timestamp);
			        				bid_value.add(-value);
			        			} else if (args[0].equals("a"))
			        			{
			        				ask_time.add(timestamp);
			        				ask_value.add(-value);
			        			} else if (args[0].equals("n"))
			        			{
			        				news_time.add(timestamp);
			        				news_value.add(-value);
			        			} else Log.w("EventGraph", "Unrecognised tuple type");
			        		} catch (NumberFormatException ex)
			        		{
			        			Log.w("Events", "Number format exception: " + ex.toString());
			        		}
			        	}
			        	else Log.w("EventGraph", "Invalid number of args");
			        }
			        
			        if (bid_time.size() > 1 && ask_time.size() > 1)
			        {
			        	// remove big gaps
			        	long maxGap = 2000;
			        	long offset = 0;
			        	int a = 0;
			        	int b = 0;
			        	int n = 0;
			        	long t = 0;
			        	while (true)
			        	{
			        		if (a < ask_time.size() && ask_time.get(a) <= t + maxGap)
			        		{
			        			long tt = ask_time.get(a);
			        			if (tt > t) t = tt;
			        			ask_time.set(a++, tt - offset);
			        		}
			        		else if (b < bid_time.size() && bid_time.get(b) <= t + maxGap)
			        		{
			        			long tt = bid_time.get(b);
			        			if (tt > t) t = tt;
			        			bid_time.set(b++, tt - offset);
			        		}
			        		else if (n < news_time.size() && news_time.get(n) <= t + maxGap)
			        		{
			        			long tt = news_time.get(n);
			        			if (tt > t) t = tt;
			        			news_time.set(n++, tt - offset);
			        		}
			        		else
			        		{
			        			long soonest = Long.MAX_VALUE;
			        			if (a < ask_time.size() && ask_time.get(a) < soonest) soonest = ask_time.get(a);
			        			if (b < bid_time.size() && bid_time.get(b) < soonest) soonest = bid_time.get(b);
			        			if (n < news_time.size() && news_time.get(n) < soonest) soonest = news_time.get(n);
			        			if (soonest != Long.MAX_VALUE)
			        			{
			        				long skip = (soonest - t) - maxGap;
			        				offset += skip;
			        				t += skip;
			        			}
			        			else
			        			{
			        				break;
			        			}
			        		}
			        	}
			        	// done removal
			        	
			        	double minV = bid_value.get(0);
			        	double maxV = bid_value.get(0);
			        	
			        	for (double v : bid_value)
			        	{
			        		if (v < minV) minV = v;
			        		if (v > maxV) maxV = v;
			        	}
			        	
			        	for (double v : ask_value)
			        	{
			        		if (v < minV) minV = v;
			        		if (v > maxV) maxV = v;
			        	}
			        	
			        	long[] bid_time_raw = new long[bid_time.size()];
			        	double[] bid_value_raw = new double[bid_time.size()];
			        	long[] ask_time_raw = new long[ask_time.size()];
			        	double[] ask_value_raw = new double[ask_time.size()];
			        	for (int i = 0; i < bid_time.size(); i++)
			        	{
			        		bid_time_raw[i] = bid_time.get(i);
			        		bid_value_raw[i] = bid_value.get(i);
			        	}
			        	
			        	for (int i = 0; i < ask_time.size(); i++)
			        	{
			        		ask_time_raw[i] = ask_time.get(i);
			        		ask_value_raw[i] = ask_value.get(i);
			        	}
			        	
			        	// pad the range to ensure it is not 0;
			        	double range = maxV - minV;
			        	range += 1;
			        	maxV += range * 0.1;
			        	minV -= range * 0.1;
			        	range = maxV - minV;
			        	// done padding
			        	
			        	lastEventResults.graphBounds = new GraphBounds(bid_time.get(0), bid_time.get(bid_time.size()-1), minV, maxV);
			        	lastEventResults.bidData = new GraphData(bid_time.size(), bid_time_raw, bid_value_raw);
			        	lastEventResults.askData = new GraphData(ask_time.size(), ask_time_raw, ask_value_raw);
			        	
			        	// make double arrowed news by duplication
			        	for (int i = 0; i < news_time.size(); i++)
			        	{
			        		if (news_value.get(i) == 0)
			        		{
			        			news_time.add(i, news_time.get(i));
			        			news_value.set(i, 0.5);
			        			news_value.add(i, -0.5);
			        			i++;
			        		}
			        	}
			        	
			        	// interpretation of news data into a usable graph form
			        	GraphIndicator[] news = new GraphIndicator[news_time.size()];
			        	for (int i = 0; i < news_time.size(); i++)
			        	{
			        		GraphIndicator gi = new GraphIndicator(news_time.get(i), 0, 0);
			        		gi.value1 = (lastEventResults.bidData.getValue(gi.time) + lastEventResults.askData.getValue(gi.time)) / 2;
			        		gi.value2 = gi.value1 + news_value.get(i) * range / 10;
			        		if (gi.value2 > maxV) gi.value2 = maxV;
			        		if (gi.value2 < minV) gi.value2 = minV;
			        		news[i] = gi;
			        	}
			        	lastEventResults.newsIndicators = news;
			        }
			        else
			        {
			        	Log.w("EventGraph", "Insufficient data");
			        }
				} catch (Exception ex) { }
			}
		}
	}
	
	public GraphBounds getGraphBounds(Event e)
	{
		synchronized (this)
		{
			updateEventQueryResults(e);
			return lastEventResults.graphBounds;
		}
	}

	public GraphData getBidData(Event e)
	{
		synchronized (this)
		{
			updateEventQueryResults(e);
			return lastEventResults.bidData;
		}
	}

	public GraphData getAskData(Event e)
	{
		synchronized (this)
		{
			updateEventQueryResults(e);
			return lastEventResults.askData;
		}
	}

	public GraphIndicator[] getNewsIndicators(Event e)
	{
		synchronized (this)
		{
			updateEventQueryResults(e);
			return lastEventResults.newsIndicators;
		}
	}

	public String getTitle(Event e)
	{
		synchronized (this)
		{
			updateEventQueryResults(e);
			return lastEventResults.title;
		}
	}
	
	private class EventQueryResults
	{
		public GraphBounds graphBounds = null;
		public GraphData bidData = null;
		public GraphData askData = null;
		public GraphIndicator[] newsIndicators = null;
		public String title = null;
	}

	public Event[] getRecentEvents(int threshold)
	{
		ArrayList<Event> events = new ArrayList<Event>(0);
		
		HttpParams params = new BasicHttpParams();
		HttpConnectionParams.setConnectionTimeout(params, 5000);
		HttpConnectionParams.setSoTimeout(params, 5000);
		HttpClient client = new DefaultHttpClient(params);

		try
		{
			long timestampMax = lastUpdateTime;
	        HttpGet request = new HttpGet();
	        request.setURI(new URI(httpServer + "/events.php?since=" + lastUpdateTime + "&severity=" + threshold));
	        HttpResponse response = client.execute(request);
	        BufferedReader in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
	        String line;
	        while ((line = in.readLine()) != null)
	        {
	        	// id,rating,company,timestamp,hour,minute
	        	String[] args = line.split(",");
	        	if (args.length == 6)
	        	{
	        		Event e = null;
	        		try
	        		{
	        			long timestamp = Long.parseLong(args[3]);
	        			if (timestamp > timestampMax) timestampMax = timestamp;
	        			e = new Event(args[2], Event.NEW, Integer.parseInt(args[0]), 0, Integer.parseInt(args[1]), System.currentTimeMillis(), Integer.parseInt(args[4]), Integer.parseInt(args[5]));
	        		} catch (NumberFormatException ex)
	        		{
	        			Log.w("Events", "Number format exception: " + ex.toString());
	        		}
	        		events.add(e);
	        	}
	        	else Log.w("Events", "Invalid number of args");
	        }
	        lastUpdateTime = timestampMax;
		} catch (Exception ex) { Log.w("EventConnect", ex.toString());}

		/* FAKE EVENT GENERATOR FOR TESTING GUI
		int fakeEvents = (int)(Math.random() * 3);
		for (int i = 0; i < fakeEvents; i++)
		{
			events.add(new Event("MKS.L", Event.NEW, 0, 0, (int)(Math.random() * 256), System.currentTimeMillis(), (int)(Math.random() * 24), (int)(Math.random() * 60)));
		}
		*/
		
		return events.toArray(new Event[events.size()]);
	}

	public void sendEventRatingFeedback(Event event, int newRating)
	{
		HttpParams params = new BasicHttpParams();
		HttpConnectionParams.setConnectionTimeout(params, 5000);
		HttpConnectionParams.setSoTimeout(params, 5000);
		HttpClient client = new DefaultHttpClient(params);
		try
		{
	        HttpGet request = new HttpGet();
	        request.setURI(new URI(httpServer + "/eventfeedback.php?id=" + event.id + "&severity=" + newRating));
	        client.execute(request);
		} catch (Exception ex) { Log.w("EventRating", "Unable to send event rating feedback"); }
	}
}
